import os
import sys
import time
import shutil
import logging
import requests
import mimetypes
from uuid import uuid4
from queue import Queue, Empty
from typing import *
from threading import Thread
from urllib.parse import urlparse

from flask import Flask, request, send_file, make_response

HOME_PATH = os.path.expanduser('~')
app = Flask(__name__)
logger = logging.getLogger("werkzeug")


class Status:
    FAILED = -1
    NOT_START = 0
    DOWNLOADING = 1
    SUCCESS = 2


CACHE_PATH = os.path.join(HOME_PATH, ".static_server", "cache_datas")
if not os.path.exists(CACHE_PATH):
    os.makedirs(CACHE_PATH, exist_ok=True)


class Url(object):
    """
        请求要用，地址在不在

        下载要用
            下载到哪呢？
    """

    def __init__(self, url):
        self.url = url
        self.url_parse = urlparse(self.url)
        origin_path_dir = os.path.dirname(self.url_parse.path)[1:]
        self.filename = os.path.basename(self.url_parse.path)
        version_msg = self.url_parse.query
        if not version_msg:
            version_msg = "default"
        self.store_dir = os.path.join(CACHE_PATH, self.url_parse.hostname,
                                      origin_path_dir, version_msg)

    @property
    def store_filepath(self):
        if not os.path.exists(self.store_dir):
            os.makedirs(self.store_dir, exist_ok=True)
        return os.path.join(self.store_dir, self.filename)


class StaticsDownloader(object):
    """
        并行下载数据，
            下载好后， 存储到指定位置，并更新 path, status

        获取数据
            查看并等待数据下载好，
                下载好后， 就返回 path

            获取也需要一个超时哦

    """
    worker_num = 10
    downloading_map = {}

    def __init__(self):
        self.req_queue = Queue()
        self.workers = []
        self.started = False

    def start(self):
        for _ in range(self.worker_num):
            self.workers.append(Thread(target=self._keep_downloading,
                                       daemon=True))
        for worker in self.workers:
            worker.start()
        self.started = True

    @classmethod
    def _handle_css_inner_url(cls, url_obj: Url, text):
        if "css" not in url_obj.url:
            return text.encode()

        if "url('../" in text:
            base_path = os.path.dirname(os.path.dirname(url_obj.url_parse.path))
            base_path = base_path[1:]  # 去除前置 /
            prefix_url = os.path.join(
                f"{url_obj.url_parse.scheme}://{url_obj.url_parse.hostname}",
                base_path)
            adjust_prefix = f"url('./static?url={prefix_url}/"
            text = text.replace("url('../", adjust_prefix)
        return text.encode()

    def _do_download(self, url_obj: Url, retry_times=3):
        try:
            logger.info(f"downloading: {url_obj.url}")
            rsp = requests.get(url_obj.url, timeout=10)

            content = self._handle_css_inner_url(url_obj, rsp.text)

            temp_path = os.path.join(CACHE_PATH, str(uuid4))
            with open(temp_path, 'wb') as fp:
                fp.write(content)
            shutil.move(temp_path, url_obj.store_filepath)
            logger.info(f"download complete: {url_obj.url} -> "
                        f"{url_obj.store_filepath}")
        except Exception as e:
            if retry_times <= 0:
                raise e
            self._do_download(url_obj, retry_times=retry_times - 1)

    def _keep_downloading(self):
        while True:
            url: Optional[Url] = None
            try:
                url: Url = self.req_queue.get(timeout=5)
                if url in self.downloading_map:
                    return
                self.downloading_map[url.url] = 1
                self._do_download(url)
            except Empty:
                continue
            finally:
                if url and url.url in self.downloading_map:
                    self.downloading_map.pop(url.url)

    def download_and_wait(self, url_obj: Url, timeout=60):
        if url_obj.url not in self.downloading_map:
            self.req_queue.put(url_obj)
        end_time = time.time() + timeout
        while time.time() <= end_time:
            if url_obj.url not in self.downloading_map and os.path.exists(
                    url_obj.store_filepath):
                return True
            time.sleep(0.1)

        raise TimeoutError(f"Timed out waiting for download: {url_obj}")


class ResourceFetcher(object):

    def __init__(self):
        self.downloader = StaticsDownloader()

    def fetch_static_resource(self, url) -> str:
        url_obj = Url(url)
        if os.path.exists(url_obj.store_filepath):
            return url_obj.store_filepath
        # 不存在， 就需要发送给 download 然后等待了

        if self.downloader.download_and_wait(url_obj):
            return url_obj.store_filepath

    def init(self):
        if not self.downloader.started:
            self.downloader.start()


rf = ResourceFetcher()


@app.route("/static", methods=["GET", "OPTIONS"])
def get_static():
    url = request.args["url"]
    method = request.method
    static_filepath = ""
    if method == "OPTIONS":
        rsp = make_response("ok", 200)
    elif method == "GET":
        static_filepath = rf.fetch_static_resource(url)
        rsp = send_file(static_filepath)
    else:
        raise ValueError(f"not allowed method: `{method}`")

    if static_filepath:
        rsp.headers["Content-Type"] = mimetypes.guess_type(static_filepath)[0]
    rsp.headers["Access-Control-Allow-Origin"] = "*"  # 设置允许跨域
    rsp.headers["Access-Control-Allow-Methods"] = "GET,POST"
    rsp.headers["Access-Control-Allow-Headers"] = ("X-Requested-With,"
                                                   "Content-Type,"
                                                   "Authorization")
    return rsp


def main():
    import argparse

    from static_server import __version__
    parser = argparse.ArgumentParser()

    parser.version = str(__version__)
    parser.add_argument('-V', action='version',
                        help='print the version and exit')
    parser.add_argument(
        "-port", help="API server port, default: 5000",
        default=5000, type=int)

    args = parser.parse_args()
    rf.init()
    app.run(host="0.0.0.0", port=args.port)


if __name__ == "__main__":
    main()
